// Copyright (C) Kumo inc. and its affiliates.
// Author: Jeff.li lijippy@163.com
// All rights reserved.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//

package operator

import (
	"context"
	"encoding/json"
	"fmt"
	"math"
	"sort"
	"strconv"
	"strings"

	"github.com/AstroProfundis/sysinfo"
	"github.com/kumose/kmopt/pkg/checkpoint"
	"github.com/kumose/kmopt/pkg/cluster/ctxt"
	"github.com/kumose/kmopt/pkg/cluster/module"
	"github.com/kumose/kmopt/pkg/cluster/spec"
	"github.com/kumose/kmopt/pkg/insight"
	"go.uber.org/zap"
)

// CheckOptions control the list of checks to be performed
type CheckOptions struct {
	// checks that are disabled by default
	EnableCPU  bool
	EnableMem  bool
	EnableDisk bool

	// pre-defined goups of checks
	// GroupMinimal bool // a minimal set of checks
}

// Names of checks
var (
	CheckNameGeneral       = "general" // errors that don't fit any specific check
	CheckNameNTP           = "ntp"
	CheckNameChrony        = "chrony"
	CheckNameOSVer         = "os-version"
	CheckNameSwap          = "swap"
	CheckNameSysctl        = "sysctl"
	CheckNameCPUThreads    = "cpu-cores"
	CheckNameCPUGovernor   = "cpu-governor"
	CheckNameDisks         = "disk"
	CheckNamePortListen    = "listening-port"
	CheckNameEpoll         = "epoll-exclusive"
	CheckNameMem           = "memory"
	CheckNameNet           = "network"
	CheckNameLimits        = "limits"
	CheckNameSysService    = "service"
	CheckNameSELinux       = "selinux"
	CheckNameCommand       = "command"
	CheckNameFio           = "fio"
	CheckNameTHP           = "thp"
	CheckNameDirPermission = "permission"
	CheckNameDirExist      = "exist"
	CheckNameTimeZone      = "timezone"
)

// CheckResult is the result of a check
type CheckResult struct {
	Name string // Name of the check
	Err  error  // An embedded error
	Warn bool   // The check didn't pass, but not a big problem
	Msg  string // A message or description
}

// Error implements the error interface
func (c CheckResult) Error() string {
	return c.Err.Error()
}

// String returns a readable string of the error
func (c CheckResult) String() string {
	return fmt.Sprintf("check failed for %s: %s", c.Name, c.Err)
}

// Unwrap implements the Wrapper interface
func (c CheckResult) Unwrap() error {
	return c.Err
}

// IsWarning checks if the result is a warning error
func (c CheckResult) IsWarning() bool {
	return c.Warn
}

// Passed checks if the result is a success
func (c CheckResult) Passed() bool {
	return c.Err == nil
}

// CheckSystemInfo performs checks with basic system info
func CheckSystemInfo(opt *CheckOptions, rawData []byte) []*CheckResult {
	var results []*CheckResult
	var insightInfo insight.Info
	if err := json.Unmarshal(rawData, &insightInfo); err != nil {
		return append(results, &CheckResult{
			Name: CheckNameGeneral,
			Err:  err,
		})
	}

	// check basic system info
	results = append(results, checkSysInfo(opt, &insightInfo.SysInfo)...)

	// check time sync status
	switch {
	case insightInfo.ChronyStat.LeapStatus != "none":
		results = append(results, checkChrony(&insightInfo.ChronyStat))
	case insightInfo.NTP.Status != "none":
		results = append(results, checkNTP(&insightInfo.NTP))
	default:
		results = append(results,
			&CheckResult{
				Name: CheckNameNTP,
				Err:  fmt.Errorf("The NTPd daemon or Chronyd daemon may be not installed"),
				Warn: true,
			},
		)
	}

	epollResult := &CheckResult{
		Name: CheckNameEpoll,
	}
	if !insightInfo.EpollExcl {
		epollResult.Err = fmt.Errorf("epoll exclusive is not supported")
	}
	results = append(results, epollResult)

	return results
}

func checkSysInfo(opt *CheckOptions, sysInfo *sysinfo.SysInfo) []*CheckResult {
	var results []*CheckResult

	results = append(results, checkOSInfo(opt, &sysInfo.OS))

	// check cpu capacities
	results = append(results, checkCPU(opt, &sysInfo.CPU)...)

	// check memory size
	results = append(results, checkMem(opt, &sysInfo.Memory)...)

	// check network
	results = append(results, checkNetwork(opt, sysInfo.Network)...)

	return results
}

// Try to keep this in sync with
//
// This information is in most cases based on the `ID` (Vendor) and `VERSION_ID` (Release) of /etc/os-release
// See https://github.com/AstroProfundis/sysinfo/blob/kmopt/os.go for details.
func checkOSInfo(opt *CheckOptions, osInfo *sysinfo.OS) *CheckResult {
	result := &CheckResult{
		Name: CheckNameOSVer,
		Msg:  fmt.Sprintf("OS is %s %s", osInfo.Name, osInfo.Release),
	}

	// check OS vendor
	switch osInfo.Vendor {
	case "kylin":
		// VERSION_ID="V10"
		if ver, _ := strconv.ParseFloat(strings.Trim(osInfo.Version, "V"), 64); ver < 10 {
			result.Err = fmt.Errorf("%s %s not supported, use version V10 or higher",
				osInfo.Name, osInfo.Release)
			return result
		}
	case "amzn":
		// https://aws.amazon.com/linux/amazon-linux-2023/
		if osInfo.Version == "2023" {
			return result
		}

		// Amazon Linux 2 is based on CentOS 7 and is recommended for
		// AWS Graviton 2 (ARM64) deployments.
		// https://aws.amazon.com/amazon-linux-2/
		if ver, _ := strconv.ParseFloat(osInfo.Version, 64); ver < 2 || ver >= 3 {
			result.Err = fmt.Errorf("%s %s not supported, use Amazon Linux 2 or Amazon Linux 2023 please",
				osInfo.Name, osInfo.Release)
			return result
		}
	case "centos":
		// CentOS Linux is EOL
		// CentOS Stream 9 and newer is still fine
		if ver, _ := strconv.ParseFloat(osInfo.Version, 64); ver < 9 {
			result.Err = fmt.Errorf("%s %s not supported, use version 9 or higher",
				osInfo.Name, osInfo.Release)
			return result
		}
	case "redhat", "rhel", "ol":
		// RHEL 8.4 or newer 8.x versions are supported
		if ver, _ := strconv.ParseFloat(osInfo.Version, 64); ver < 8.4 || ver >= 9 {
			result.Err = fmt.Errorf("%s %s not supported, use version 8.4 or a later 8.x version please",
				osInfo.Name, osInfo.Release)
			return result
		}
	case "rocky":
		// Rocky Linux
		if ver, _ := strconv.ParseFloat(osInfo.Version, 64); ver < 9.1 {
			result.Err = fmt.Errorf("%s %s not supported, use version 9.1 or later please",
				osInfo.Name, osInfo.Release)
			return result
		}
	case "debian":
		// debian support is not fully tested, but we suppose it should work
		msg := "Debian support is not fully tested, be careful"
		result.Err = fmt.Errorf("%s (%s)", result.Msg, msg)
		result.Warn = true
		if ver, _ := strconv.ParseFloat(osInfo.Version, 64); ver < 10 {
			result.Err = fmt.Errorf("%s %s not supported, use version 10 or higher (%s)",
				osInfo.Name, osInfo.Release, msg)
			result.Warn = false
			return result
		}
	case "ubuntu":
		// ubuntu support is not fully tested, but we suppose it should work
		msg := "Ubuntu support is not fully tested, be careful"
		result.Err = fmt.Errorf("%s (%s)", result.Msg, msg)
		result.Warn = true
		if ver, _ := strconv.ParseFloat(osInfo.Version, 64); ver < 20.04 {
			result.Err = fmt.Errorf("%s %s not supported, use version 20.04 or higher (%s)",
				osInfo.Name, osInfo.Release, msg)
			result.Warn = false
			return result
		}
	case "openEuler":
		return result
	default:
		result.Err = fmt.Errorf("OS vendor %s not supported", osInfo.Vendor)
		return result
	}

	// TODO: check OS architecture

	return result
}

func checkNTP(ntpInfo *insight.TimeStat) *CheckResult {
	result := &CheckResult{
		Name: CheckNameNTP,
	}

	if ntpInfo.Status == "none" {
		zap.L().Info("The NTPd daemon may be not installed, skip.")
		return result
	}

	if ntpInfo.Sync == "none" {
		result.Err = fmt.Errorf("The NTPd daemon may be not start")
		result.Warn = true
		return result
	}

	// check if time offset greater than +- 500ms
	if math.Abs(ntpInfo.Offset) >= 500 {
		result.Err = fmt.Errorf("time offset %fms too high", ntpInfo.Offset)
	}
	return result
}

func checkChrony(chronyInfo *insight.ChronyStat) *CheckResult {
	result := &CheckResult{
		Name: CheckNameChrony,
	}

	if chronyInfo.LeapStatus == "none" {
		zap.L().Info("The Chrony daemon may be not installed, skip.")
		return result
	}

	// check if time offset greater than +- 500ms
	if math.Abs(chronyInfo.LastOffset) >= 500 {
		result.Err = fmt.Errorf("time offset %fms too high", chronyInfo.LastOffset)
	}
	return result
}

func checkCPU(opt *CheckOptions, cpuInfo *sysinfo.CPU) []*CheckResult {
	var results []*CheckResult
	if opt.EnableCPU && cpuInfo.Threads < 16 {
		results = append(results, &CheckResult{
			Name: CheckNameCPUThreads,
			Err:  fmt.Errorf("CPU thread count %d too low, needs 16 or more", cpuInfo.Threads),
		})
	} else {
		results = append(results, &CheckResult{
			Name: CheckNameCPUThreads,
			Msg:  fmt.Sprintf("number of CPU cores / threads: %d", cpuInfo.Threads),
		})
	}

	// check for CPU frequency governor
	if cpuInfo.Governor != "" {
		if cpuInfo.Governor != "performance" {
			results = append(results, &CheckResult{
				Name: CheckNameCPUGovernor,
				Err:  fmt.Errorf("CPU frequency governor is %s, should use performance", cpuInfo.Governor),
			})
		} else {
			results = append(results, &CheckResult{
				Name: CheckNameCPUGovernor,
				Msg:  fmt.Sprintf("CPU frequency governor is %s", cpuInfo.Governor),
			})
		}
	} else {
		results = append(results, &CheckResult{
			Name: CheckNameCPUGovernor,
			Err:  fmt.Errorf("Unable to determine current CPU frequency governor policy"),
			Warn: true,
		})
	}

	return results
}

func checkMem(opt *CheckOptions, memInfo *sysinfo.Memory) []*CheckResult {
	var results []*CheckResult
	if memInfo.Swap > 0 {
		results = append(results, &CheckResult{
			Name: CheckNameSwap,
			Warn: true,
			Err:  fmt.Errorf("swap is enabled, please disable it for best performance"),
		})
	}

	// 32GB
	if opt.EnableMem && memInfo.Size < 1024*32 {
		results = append(results, &CheckResult{
			Name: CheckNameMem,
			Err:  fmt.Errorf("memory size %dMB too low, needs 32GB or more", memInfo.Size),
		})
	} else {
		results = append(results, &CheckResult{
			Name: CheckNameMem,
			Msg:  fmt.Sprintf("memory size is %dMB", memInfo.Size),
		})
	}

	return results
}

func checkNetwork(opt *CheckOptions, networkDevices []sysinfo.NetworkDevice) []*CheckResult {
	var results []*CheckResult
	for _, netdev := range networkDevices {
		// ignore the network devices that cannot be detected
		if netdev.Speed == 0 {
			continue
		}
		if netdev.Speed >= 1000 {
			results = append(results, &CheckResult{
				Name: CheckNameNet,
				Msg:  fmt.Sprintf("network speed of %s is %dMB", netdev.Name, netdev.Speed),
			})
		} else {
			results = append(results, &CheckResult{
				Name: CheckNameNet,
				Err:  fmt.Errorf("network speed of %s is %dMB too low, needs 1GB or more", netdev.Name, netdev.Speed),
			})
		}
	}

	return results
}

// CheckSysLimits checks limits in /etc/security/limits.conf
func CheckSysLimits(opt *CheckOptions, user string, l []byte) []*CheckResult {
	var results []*CheckResult

	var (
		stackSoft  int
		nofileSoft int
		nofileHard int
	)

	for line := range strings.SplitSeq(string(l), "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "#") {
			continue
		}

		fields := strings.Fields(line)
		if len(fields) < 3 || fields[0] != user {
			continue
		}

		switch fields[2] {
		case "nofile":
			if fields[1] == "soft" {
				nofileSoft, _ = strconv.Atoi(fields[3])
			} else {
				nofileHard, _ = strconv.Atoi(fields[3])
			}
		case "stack":
			if fields[1] == "soft" {
				stackSoft, _ = strconv.Atoi(fields[3])
			}
		}
	}

	if nofileSoft < 1000000 {
		results = append(results, &CheckResult{
			Name: CheckNameLimits,
			Err:  fmt.Errorf("soft limit of 'nofile' for user '%s' is not set or too low", user),
			Msg:  fmt.Sprintf("%s    soft    nofile    1000000", user),
		})
	}
	if nofileHard < 1000000 {
		results = append(results, &CheckResult{
			Name: CheckNameLimits,
			Err:  fmt.Errorf("hard limit of 'nofile' for user '%s' is not set or too low", user),
			Msg:  fmt.Sprintf("%s    hard    nofile    1000000", user),
		})
	}
	if stackSoft < 10240 {
		results = append(results, &CheckResult{
			Name: CheckNameLimits,
			Err:  fmt.Errorf("soft limit of 'stack' for user '%s' is not set or too low", user),
			Msg:  fmt.Sprintf("%s    soft    stack    10240", user),
		})
	}

	// all pass
	if len(results) < 1 {
		results = append(results, &CheckResult{
			Name: CheckNameLimits,
		})
	}

	return results
}

// CheckKernelParameters checks kernel parameter values
func CheckKernelParameters(opt *CheckOptions, p []byte) []*CheckResult {
	var results []*CheckResult

	for line := range strings.SplitSeq(string(p), "\n") {
		line = strings.TrimSpace(line)
		fields := strings.Fields(line)
		if len(fields) < 3 {
			continue
		}

		switch fields[0] {
		case "fs.file-max":
			val, _ := strconv.Atoi(fields[2])
			if val < 1000000 {
				results = append(results, &CheckResult{
					Name: CheckNameSysctl,
					Err:  fmt.Errorf("fs.file-max = %d, should be greater than 1000000", val),
					Msg:  "fs.file-max = 1000000",
				})
			}
		case "net.core.somaxconn":
			val, _ := strconv.Atoi(fields[2])
			if val < 32768 {
				results = append(results, &CheckResult{
					Name: CheckNameSysctl,
					Err:  fmt.Errorf("net.core.somaxconn = %d, should 32768 or greater", val),
					Msg:  "net.core.somaxconn = 32768",
				})
			}
		case "net.ipv4.tcp_tw_recycle":
			val, _ := strconv.Atoi(fields[2])
			if val != 0 {
				results = append(results, &CheckResult{
					Name: CheckNameSysctl,
					Err:  fmt.Errorf("net.ipv4.tcp_tw_recycle = %d, should be 0", val),
					Msg:  "net.ipv4.tcp_tw_recycle = 0",
				})
			}
		case "net.ipv4.tcp_syncookies":
			val, _ := strconv.Atoi(fields[2])
			if val != 0 {
				results = append(results, &CheckResult{
					Name: CheckNameSysctl,
					Err:  fmt.Errorf("net.ipv4.tcp_syncookies = %d, should be 0", val),
					Msg:  "net.ipv4.tcp_syncookies = 0",
				})
			}
		case "vm.overcommit_memory":
			val, _ := strconv.Atoi(fields[2])
			if opt.EnableMem && val != 0 && val != 1 {
				results = append(results, &CheckResult{
					Name: CheckNameSysctl,
					Err:  fmt.Errorf("vm.overcommit_memory = %d, should be 0 or 1", val),
					Msg:  "vm.overcommit_memory = 1",
				})
			}
		case "vm.swappiness":
			val, _ := strconv.Atoi(fields[2])
			if val != 0 {
				results = append(results, &CheckResult{
					Name: CheckNameSysctl,
					Err:  fmt.Errorf("vm.swappiness = %d, should be 0", val),
					Msg:  "vm.swappiness = 0",
				})
			}
		}
	}

	// all pass
	if len(results) < 1 {
		results = append(results, &CheckResult{
			Name: CheckNameSysctl,
		})
	}

	return results
}

// CheckServices checks if a service is running on the host
func CheckServices(ctx context.Context, e ctxt.Executor, host, service string, disable bool, systemdMode spec.SystemdMode) *CheckResult {
	result := &CheckResult{
		Name: CheckNameSysService,
	}

	// check if the service exist before checking its status, ignore when non-exist
	stdout, _, err := e.Execute(
		ctx,
		fmt.Sprintf(
			"systemctl list-unit-files --type service | grep -i %s.service | wc -l", service),
		systemdMode != spec.UserMode)
	if err != nil {
		result.Err = err
		return result
	}
	if cnt, _ := strconv.Atoi(strings.Trim(string(stdout), "\n")); cnt == 0 {
		if !disable {
			result.Err = fmt.Errorf("service %s not found, should be installed and started", service)
		}
		result.Msg = fmt.Sprintf("service %s not found, ignore", service)
		return result
	}
	// The service checked here needs to use systemctl in system mode, so the value passed by scope is empty.
	active, _, _, err := GetServiceStatus(ctx, e, service+".service", "", string(systemdMode))
	if err != nil {
		result.Err = err
	}

	switch disable {
	case false:
		if active != "active" {
			result.Err = fmt.Errorf("service %s is not running", service)
			result.Msg = fmt.Sprintf("start %s.service", service)
		}
	case true:
		if active == "active" {
			result.Err = fmt.Errorf("service %s is running but should be stopped", service)
			result.Msg = fmt.Sprintf("stop %s.service", service)
		}
	}

	return result
}

// CheckSELinux checks if SELinux is enabled on the host
func CheckSELinux(ctx context.Context, e ctxt.Executor, sudo bool) *CheckResult {
	result := &CheckResult{
		Name: CheckNameSELinux,
	}
	m := module.NewShellModule(module.ShellModuleConfig{
		// ignore grep errors, the file may not exist for some systems
		Command: "grep -E '^\\s*SELINUX=enforcing' /etc/selinux/config 2>/dev/null | wc -l",
		Sudo:    sudo,
	})
	stdout, stderr, err := m.Execute(ctx, e)
	if err != nil {
		result.Err = fmt.Errorf("%w %s", err, stderr)
		return result
	}
	out := strings.Trim(string(stdout), "\n")
	lines, err := strconv.Atoi(out)
	if err != nil {
		result.Err = fmt.Errorf("can not check SELinux status, please validate manually, %s", err)
		result.Warn = true
		return result
	}

	if lines > 0 {
		result.Err = fmt.Errorf("SELinux is not disabled")
	} else {
		result.Msg = "SELinux is disabled"
	}
	return result
}

// CheckListeningPort checks if the ports are already binded by some process on host
func CheckListeningPort(opt *CheckOptions, host string, topo *spec.Specification, rawData []byte) []*CheckResult {
	var results []*CheckResult
	ports := make(map[int]struct{})

	topo.IterInstance(func(inst spec.Instance) {
		if inst.GetManageHost() != host {
			return
		}
		for _, up := range inst.UsedPorts() {
			if _, found := ports[up]; !found {
				ports[up] = struct{}{}
			}
		}
	})

	for p := range ports {
		for line := range strings.SplitSeq(string(rawData), "\n") {
			fields := strings.Fields(line)
			if len(fields) < 5 || fields[0] != "LISTEN" {
				continue
			}
			addr := strings.Split(fields[3], ":")
			lp, _ := strconv.Atoi(addr[len(addr)-1])
			if p == lp {
				results = append(results, &CheckResult{
					Name: CheckNamePortListen,
					Err:  fmt.Errorf("port %d is already in use", lp),
				})
				break // ss may report multiple entries for the same port
			}
		}
	}
	return results
}

// CheckPartitions checks partition info of data directories
func CheckPartitions(opt *CheckOptions, host string, topo *spec.Specification, rawData []byte) []*CheckResult {
	var results []*CheckResult
	var insightInfo insight.Info
	if err := json.Unmarshal(rawData, &insightInfo); err != nil {
		return append(results, &CheckResult{
			Name: CheckNameDisks,
			Err:  err,
		})
	}

	flt := flatPartitions(insightInfo.Partitions)
	parts := sortPartitions(flt)

	// check if multiple instances are using the same partition as data storage
	type storePartitionInfo struct {
		comp string
		path string
	}
	uniqueStores := make(map[string][]storePartitionInfo) // host+partition -> info

	topo.IterInstance(func(inst spec.Instance) {
		if inst.GetManageHost() != host {
			return
		}
		for _, dataDir := range spec.MultiDirAbs(topo.GlobalOptions.User, inst.DataDir()) {
			if dataDir == "" {
				continue
			}

			blk := getDisk(parts, dataDir)
			if blk == nil {
				return
			}

			// only check for TiKV and TiFlash, other components are not that I/O sensitive
			switch inst.ComponentName() {
			case spec.ComponentTiKV,
				spec.ComponentTiFlash:
				usKey := fmt.Sprintf("%s:%s", host, blk.Mount.MountPoint)
				uniqueStores[usKey] = append(uniqueStores[usKey], storePartitionInfo{
					comp: inst.ComponentName(),
					path: dataDir,
				})
			}

			switch blk.Mount.FSType {
			case "ext4":
				if !strings.Contains(blk.Mount.Options, "nodelalloc") {
					results = append(results, &CheckResult{
						Name: CheckNameDisks,
						Err:  fmt.Errorf("mount point %s does not have 'nodelalloc' option set", blk.Mount.MountPoint),
					})
				}
				fallthrough
			case "xfs":
				if !strings.Contains(blk.Mount.Options, "noatime") {
					results = append(results, &CheckResult{
						Name: CheckNameDisks,
						Err:  fmt.Errorf("mount point %s does not have 'noatime' option set", blk.Mount.MountPoint),
						Warn: true,
					})
				}
			default:
				results = append(results, &CheckResult{
					Name: CheckNameDisks,
					Err: fmt.Errorf("mount point %s has an unsupported filesystem '%s'",
						blk.Mount.MountPoint, blk.Mount.FSType),
				})
			}
		}
	})

	for key, parts := range uniqueStores {
		if len(parts) > 1 {
			pathList := make([]string, 0)
			for _, p := range parts {
				pathList = append(pathList,
					fmt.Sprintf("%s:%s", p.comp, p.path),
				)
			}
			results = append(results, &CheckResult{
				Name: CheckNameDisks,
				Err: fmt.Errorf(
					"multiple components %s are using the same partition %s as data dir",
					strings.Join(pathList, ","),
					key,
				),
			})
		}
	}

	return results
}

func flatPartitions(parts []insight.BlockDev) []insight.BlockDev {
	var flatBlk []insight.BlockDev
	for _, blk := range parts {
		if len(blk.SubDev) > 0 {
			flatBlk = append(flatBlk, flatPartitions(blk.SubDev)...)
		}
		// blocks with empty mount points are ignored
		if blk.Mount.MountPoint != "" {
			flatBlk = append(flatBlk, blk)
		}
	}
	return flatBlk
}

func sortPartitions(parts []insight.BlockDev) []insight.BlockDev {
	// The longest mount point is at top of the list
	sort.Slice(parts, func(i, j int) bool {
		return len(parts[i].Mount.MountPoint) > len(parts[j].Mount.MountPoint)
	})

	return parts
}

// getDisk find the first block dev from the list that matches the given path
func getDisk(parts []insight.BlockDev, fullpath string) *insight.BlockDev {
	for _, blk := range parts {
		if strings.HasPrefix(fullpath, blk.Mount.MountPoint) {
			return &blk
		}
	}
	return nil
}

// CheckFIOResult parses and checks the result of fio test
func CheckFIOResult(rr, rw, lat []byte) []*CheckResult {
	var results []*CheckResult

	// check results for rand read test
	var rrRes map[string]any
	if err := json.Unmarshal(rr, &rrRes); err != nil {
		results = append(results, &CheckResult{
			Name: CheckNameFio,
			Err:  fmt.Errorf("error parsing result of random read test, %s", err),
		})
	} else if jobs, ok := rrRes["jobs"]; ok {
		readRes := jobs.([]any)[0].(map[string]any)["read"]
		readIOPS := readRes.(map[string]any)["iops"]

		results = append(results, &CheckResult{
			Name: CheckNameFio,
			Msg:  fmt.Sprintf("IOPS of random read: %f", readIOPS.(float64)),
		})
	} else {
		results = append(results, &CheckResult{
			Name: CheckNameFio,
			Err:  fmt.Errorf("error parsing result of random read test"),
		})
	}

	// check results for rand read write
	var rwRes map[string]any
	if err := json.Unmarshal(rw, &rwRes); err != nil {
		results = append(results, &CheckResult{
			Name: CheckNameFio,
			Err:  fmt.Errorf("error parsing result of random read write test, %s", err),
		})
	} else if jobs, ok := rwRes["jobs"]; ok {
		readRes := jobs.([]any)[0].(map[string]any)["read"]
		readIOPS := readRes.(map[string]any)["iops"]

		writeRes := jobs.([]any)[0].(map[string]any)["write"]
		writeIOPS := writeRes.(map[string]any)["iops"]

		results = append(results, &CheckResult{
			Name: CheckNameFio,
			Msg:  fmt.Sprintf("IOPS of random read: %f, write: %f", readIOPS.(float64), writeIOPS.(float64)),
		})
	} else {
		results = append(results, &CheckResult{
			Name: CheckNameFio,
			Err:  fmt.Errorf("error parsing result of random read write test"),
		})
	}

	// check results for read write latency
	var latRes map[string]any
	if err := json.Unmarshal(lat, &latRes); err != nil {
		results = append(results, &CheckResult{
			Name: CheckNameFio,
			Err:  fmt.Errorf("error parsing result of read write latency test, %s", err),
		})
	} else if jobs, ok := latRes["jobs"]; ok {
		readRes := jobs.([]any)[0].(map[string]any)["read"]
		readLat := readRes.(map[string]any)["lat_ns"]
		readLatAvg := readLat.(map[string]any)["mean"]

		writeRes := jobs.([]any)[0].(map[string]any)["write"]
		writeLat := writeRes.(map[string]any)["lat_ns"]
		writeLatAvg := writeLat.(map[string]any)["mean"]

		results = append(results, &CheckResult{
			Name: CheckNameFio,
			Msg:  fmt.Sprintf("Latency of random read: %fns, write: %fns", readLatAvg.(float64), writeLatAvg.(float64)),
		})
	} else {
		results = append(results, &CheckResult{
			Name: CheckNameFio,
			Err:  fmt.Errorf("error parsing result of read write latency test"),
		})
	}

	return results
}

// CheckTHP checks THP in /sys/kernel/mm/transparent_hugepage/enabled
func CheckTHP(ctx context.Context, e ctxt.Executor, sudo bool) *CheckResult {
	result := &CheckResult{
		Name: CheckNameTHP,
	}

	m := module.NewShellModule(module.ShellModuleConfig{
		Command: fmt.Sprintf(`if [ -d %[1]s ]; then cat %[1]s/enabled; fi`, "/sys/kernel/mm/transparent_hugepage"),
		Sudo:    sudo,
	})
	stdout, stderr, err := m.Execute(ctx, e)
	if err != nil {
		result.Err = fmt.Errorf("%w %s", err, stderr)
		return result
	}

	for line := range strings.SplitSeq(strings.Trim(string(stdout), "\n"), "\n") {
		if len(line) > 0 && !strings.Contains(line, "[never]") {
			result.Err = fmt.Errorf("THP is enabled, please disable it for best performance")
			return result
		}
	}

	result.Msg = "THP is disabled"
	return result
}

// CheckJRE checks if java command is available for TiSpark nodes
func CheckJRE(ctx context.Context, e ctxt.Executor, host string, topo *spec.Specification) []*CheckResult {
	var results []*CheckResult

	topo.IterInstance(func(inst spec.Instance) {
		if inst.ComponentName() != spec.ComponentTiSpark {
			return
		}

		// check if java cli is available
		// the checkpoint part of context can't be shared between goroutines
		stdout, stderr, err := e.Execute(checkpoint.NewContext(ctx), "java -version", false)
		if err != nil {
			results = append(results, &CheckResult{
				Name: CheckNameCommand,
				Err:  fmt.Errorf("java not usable, %s", strings.Trim(string(stderr), "\n")),
				Msg:  "JRE is not installed properly or not set in PATH",
			})
			return
		}
		if len(stderr) > 0 {
			// java -version returns as below:
			// openjdk version "1.8.0_265"
			// openjdk version "11.0.8" 2020-07-14
			line := strings.Split(string(stderr), "\n")[0]
			fields := strings.Split(line, `"`)
			ver := strings.TrimSpace(fields[1])
			if strings.Compare(ver, "1.8") < 0 {
				results = append(results, &CheckResult{
					Name: CheckNameCommand,
					Err:  fmt.Errorf("java version %s is not supported, use Java 8 (1.8)+", ver),
					Msg:  "Installed JRE is not Java 8+",
				})
			} else {
				results = append(results, &CheckResult{
					Name: CheckNameCommand,
					Msg:  "java: " + strings.Split(string(stderr), "\n")[0],
				})
			}
		} else {
			results = append(results, &CheckResult{
				Name: CheckNameCommand,
				Err:  fmt.Errorf("unknown output of java %s", stdout),
				Msg:  "java: " + strings.Split(string(stdout), "\n")[0],
				Warn: true,
			})
		}
	})

	return results
}

// CheckDirPermission checks if the user can write to given path
func CheckDirPermission(ctx context.Context, e ctxt.Executor, user, path string) []*CheckResult {
	var results []*CheckResult

	_, stderr, err := e.Execute(ctx,
		fmt.Sprintf(
			"/usr/bin/sudo -u %[1]s touch %[2]s/.kmopt_cluster_check_file && rm -f %[2]s/.kmopt_cluster_check_file",
			user,
			path,
		),
		false)
	if err != nil || len(stderr) > 0 {
		results = append(results, &CheckResult{
			Name: CheckNameDirPermission,
			Err:  fmt.Errorf("unable to write to dir %s: %s", path, strings.Split(string(stderr), "\n")[0]),
			Msg:  fmt.Sprintf("%s: %s", path, err),
		})
	} else {
		results = append(results, &CheckResult{
			Name: CheckNameDirPermission,
			Msg:  fmt.Sprintf("%s is writable", path),
		})
	}

	return results
}

// CheckDirIsExist check if the directory exists
func CheckDirIsExist(ctx context.Context, e ctxt.Executor, path string) []*CheckResult {
	var results []*CheckResult

	if path == "" {
		return results
	}

	req, _, _ := e.Execute(ctx,
		fmt.Sprintf(
			"[ -e %s ] && echo 1",
			path,
		),
		false)

	if strings.ReplaceAll(string(req), "\n", "") == "1" {
		results = append(results, &CheckResult{
			Name: CheckNameDirExist,
			Err:  fmt.Errorf("%s already exists", path),
			Msg:  fmt.Sprintf("%s already exists", path),
		})
	}

	return results
}

// CheckTimeZone performs checks if time zone is the same
func CheckTimeZone(ctx context.Context, topo *spec.Specification, host string, rawData []byte) []*CheckResult {
	var results []*CheckResult
	var insightInfo, pd0insightInfo insight.Info
	if err := json.Unmarshal(rawData, &insightInfo); err != nil {
		return append(results, &CheckResult{
			Name: CheckNameTimeZone,
			Err:  err,
		})
	}

	if len(topo.PDServers) < 1 {
		return append(results, &CheckResult{
			Name: CheckNameTimeZone,
			Err:  fmt.Errorf("no pd found"),
		})
	}
	// skip compare with itself
	if topo.PDServers[0].Host == host {
		return nil
	}
	pd0stdout, _, _ := ctxt.GetInner(ctx).GetOutputs(topo.PDServers[0].Host)
	if err := json.Unmarshal(pd0stdout, &pd0insightInfo); err != nil {
		return append(results, &CheckResult{
			Name: CheckNameTimeZone,
			Err:  err,
		})
	}

	timezone := insightInfo.SysInfo.Node.Timezone
	pd0timezone := pd0insightInfo.SysInfo.Node.Timezone

	if timezone == pd0timezone {
		results = append(results, &CheckResult{
			Name: CheckNameTimeZone,
			Msg:  "time zone is the same as the first PD machine: " + timezone,
		})
	} else {
		results = append(results, &CheckResult{
			Name: CheckNameTimeZone,
			Err:  fmt.Errorf("time zone is %s, but the firt PD is %s", timezone, pd0timezone),
		})
	}
	return results
}
